/**************************************************************************
**
** Copyright (C) 2012, 2013 BlackBerry Limited. All rights reserved.
**
** Contact: BlackBerry (qt@blackberry.com)
** Contact: KDAB (info@kdab.com)
**
** This file is part of Qt Creator.
**
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and Digia.  For licensing terms and
** conditions see http://qt.digia.com/licensing.  For further information
** use the contact form at http://qt.digia.com/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 2.1 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL included in the
** packaging of this file.  Please review the following information to
** ensure the GNU Lesser General Public License version 2.1 requirements
** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** In addition, as a special exception, Digia gives you certain additional
** rights.  These rights are described in the Digia Qt LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
****************************************************************************/

#include "blackberryapplicationrunner.h"

#include "blackberrydeployconfiguration.h"
#include "blackberrydeviceconnectionmanager.h"
#include "blackberryrunconfiguration.h"
#include "blackberrylogprocessrunner.h"
#include "qnxconstants.h"

#include <projectexplorer/target.h>
#include <qmakeprojectmanager/qmakebuildconfiguration.h>
#include <ssh/sshremoteprocessrunner.h>
#include <utils/qtcassert.h>

#include <QTimer>
#include <QDir>

namespace {
bool parseRunningState(const QString &line)
{
    QTC_ASSERT(line.startsWith(QLatin1String("result::")), return false);
    return line.trimmed().mid(8) == QLatin1String("true");
}
}

using namespace ProjectExplorer;
using namespace Qnx;
using namespace Qnx::Internal;

BlackBerryApplicationRunner::BlackBerryApplicationRunner(bool debugMode, BlackBerryRunConfiguration *runConfiguration, QObject *parent)
    : QObject(parent)
    , m_debugMode(debugMode)
    , m_pid(-1)
    , m_appId(QString())
    , m_running(false)
    , m_stopping(false)
    , m_launchProcess(0)
    , m_stopProcess(0)
    , m_logProcessRunner(0)
    , m_runningStateTimer(new QTimer(this))
    , m_runningStateProcess(0)
{
    QTC_ASSERT(runConfiguration, return);

    Target *target = runConfiguration->target();
    BuildConfiguration *buildConfig = target->activeBuildConfiguration();
    m_environment = buildConfig->environment();
    m_deployCmd = m_environment.searchInPath(QLatin1String(Constants::QNX_BLACKBERRY_DEPLOY_CMD));

    m_device = BlackBerryDeviceConfiguration::device(target->kit());
    m_barPackage = runConfiguration->barPackage();

    // The BlackBerry device always uses key authentication
    m_sshParams = m_device->sshParameters();
    m_sshParams.authenticationType = QSsh::SshConnectionParameters::AuthenticationTypePublicKey;

    m_runningStateTimer->setInterval(3000);
    m_runningStateTimer->setSingleShot(true);
    connect(m_runningStateTimer, SIGNAL(timeout()), this, SLOT(determineRunningState()));
    connect(this, SIGNAL(started()), this, SLOT(startLogProcessRunner()));

    connect(&m_launchStopProcessParser, SIGNAL(pidParsed(qint64)), this, SLOT(setPid(qint64)));
    connect(&m_launchStopProcessParser, SIGNAL(applicationIdParsed(QString)), this, SLOT(setApplicationId(QString)));
}

void BlackBerryApplicationRunner::start()
{
    if (!BlackBerryDeviceConnectionManager::instance()->isConnected(m_device->id())) {
        connect(BlackBerryDeviceConnectionManager::instance(), SIGNAL(deviceConnected()),
                this, SLOT(launchApplication()));
        connect(BlackBerryDeviceConnectionManager::instance(), SIGNAL(deviceDisconnected(Core::Id)),
                this, SLOT(disconnectFromDeviceSignals(Core::Id)));
        connect(BlackBerryDeviceConnectionManager::instance(), SIGNAL(connectionOutput(Core::Id,QString)),
                this, SLOT(displayConnectionOutput(Core::Id,QString)));
        BlackBerryDeviceConnectionManager::instance()->connectDevice(m_device->id());
    } else {
        launchApplication();
    }
}

void BlackBerryApplicationRunner::startLogProcessRunner()
{
    if (!m_logProcessRunner) {
        m_logProcessRunner = new BlackBerryLogProcessRunner(this, m_appId, m_device);
        connect(m_logProcessRunner, SIGNAL(output(QString,Utils::OutputFormat)),
            this, SIGNAL(output(QString,Utils::OutputFormat)));
        connect(m_logProcessRunner, SIGNAL(finished()), this, SIGNAL(finished()));
    }

    m_logProcessRunner->start();
}

void BlackBerryApplicationRunner::displayConnectionOutput(Core::Id deviceId, const QString &msg)
{
    if (deviceId != m_device->id())
        return;

    if (msg.contains(QLatin1String("Info:")))
        emit output(msg, Utils::StdOutFormat);
    else if (msg.contains(QLatin1String("Error:")))
        emit output(msg, Utils::StdErrFormat);
}

void BlackBerryApplicationRunner::startFinished(int exitCode, QProcess::ExitStatus exitStatus)
{
    if (exitCode == 0 && exitStatus == QProcess::NormalExit && m_pid > -1) {
        emit started();
    } else {
        m_running = false;
        m_runningStateTimer->stop();

        QTC_ASSERT(m_launchProcess, return);
        const QString errorString = (m_launchProcess->error() != QProcess::UnknownError)
                ? m_launchProcess->errorString() : tr("Launching application failed");
        emit startFailed(errorString);
        reset();
    }
}

ProjectExplorer::RunControl::StopResult BlackBerryApplicationRunner::stop()
{
    if (m_stopping)
        return ProjectExplorer::RunControl::AsynchronousStop;

    m_stopping = true;

    QStringList args;
    args << QLatin1String("-terminateApp");
    args << QLatin1String("-device") << m_sshParams.host;
    if (!m_sshParams.password.isEmpty())
        args << QLatin1String("-password") << m_sshParams.password;
    args << m_barPackage;

    if (!m_stopProcess) {
        m_stopProcess = new QProcess(this);
        connect(m_stopProcess, SIGNAL(readyReadStandardError()), this, SLOT(readStandardError()));
        connect(m_stopProcess, SIGNAL(readyReadStandardOutput()), this, SLOT(readStandardOutput()));
        connect(m_stopProcess, SIGNAL(finished(int,QProcess::ExitStatus)),
                this, SLOT(stopFinished(int,QProcess::ExitStatus)));

        m_stopProcess->setEnvironment(m_environment.toStringList());
    }

    m_stopProcess->start(m_deployCmd, args);
    return ProjectExplorer::RunControl::AsynchronousStop;
}

bool BlackBerryApplicationRunner::isRunning() const
{
    return m_running;
}

qint64 BlackBerryApplicationRunner::pid() const
{
    return m_pid;
}

void BlackBerryApplicationRunner::stopFinished(int exitCode, QProcess::ExitStatus exitStatus)
{
    Q_UNUSED(exitCode);
    Q_UNUSED(exitStatus);

    reset();
}

void BlackBerryApplicationRunner::readStandardOutput()
{
    QProcess *process = qobject_cast<QProcess *>(sender());
    process->setReadChannel(QProcess::StandardOutput);
    while (process->canReadLine()) {
        QString line = QString::fromLocal8Bit(process->readLine());
        m_launchStopProcessParser.stdOutput(line);
        emit output(line, Utils::StdOutFormat);
    }
}

void BlackBerryApplicationRunner::readStandardError()
{
    QProcess *process = qobject_cast<QProcess *>(sender());
    process->setReadChannel(QProcess::StandardError);
    while (process->canReadLine()) {
        const QString line = QString::fromLocal8Bit(process->readLine());
        m_launchStopProcessParser.stdError(line);
        emit output(line, Utils::StdErrFormat);
    }
}

void BlackBerryApplicationRunner::disconnectFromDeviceSignals(Core::Id deviceId)
{
    if (m_device->id() == deviceId) {
        disconnect(BlackBerryDeviceConnectionManager::instance(), SIGNAL(deviceConnected()),
                   this, SLOT(launchApplication()));
        disconnect(BlackBerryDeviceConnectionManager::instance(), SIGNAL(deviceDisconnected(Core::Id)),
                   this, SLOT(disconnectFromDeviceSignals(Core::Id)));
        disconnect(BlackBerryDeviceConnectionManager::instance(), SIGNAL(connectionOutput(Core::Id,QString)),
                   this, SLOT(displayConnectionOutput(Core::Id,QString)));
    }
}

void BlackBerryApplicationRunner::setPid(qint64 pid)
{
    m_pid = pid;
}

void BlackBerryApplicationRunner::setApplicationId(const QString &applicationId)
{
    m_appId = applicationId;
}

void BlackBerryApplicationRunner::launchApplication()
{
    // If original device connection fails before launching, this method maybe triggered
    // if any other device is connected(?)
    if (!BlackBerryDeviceConnectionManager::instance()->isConnected(m_device->id()))
        return;

    QStringList args;
    args << QLatin1String("-launchApp");
    if (m_debugMode)
        args << QLatin1String("-debugNative");
    args << QLatin1String("-device") << m_sshParams.host;
    if (!m_sshParams.password.isEmpty())
        args << QLatin1String("-password") << m_sshParams.password;
    args << QDir::toNativeSeparators(m_barPackage);

    if (!m_launchProcess) {
        m_launchProcess = new QProcess(this);
        connect(m_launchProcess, SIGNAL(readyReadStandardError()), this, SLOT(readStandardError()));
        connect(m_launchProcess, SIGNAL(readyReadStandardOutput()), this, SLOT(readStandardOutput()));
        connect(m_launchProcess, SIGNAL(finished(int,QProcess::ExitStatus)),
                this, SLOT(startFinished(int,QProcess::ExitStatus)));

        m_launchProcess->setEnvironment(m_environment.toStringList());
    }

    m_launchProcess->start(m_deployCmd, args);
    m_runningStateTimer->start();
    m_running = true;
}

void BlackBerryApplicationRunner::startRunningStateTimer()
{
    if (m_running)
        m_runningStateTimer->start();
}

void BlackBerryApplicationRunner::determineRunningState()
{
    QStringList args;
    args << QLatin1String("-isAppRunning");
    args << QLatin1String("-device") << m_sshParams.host;
    if (!m_sshParams.password.isEmpty())
        args << QLatin1String("-password") << m_sshParams.password;
    args << m_barPackage;

    if (!m_runningStateProcess) {
        m_runningStateProcess = new QProcess(this);

        connect(m_runningStateProcess, SIGNAL(readyReadStandardOutput()), this, SLOT(readRunningStateStandardOutput()));
        connect(m_runningStateProcess, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(startRunningStateTimer()));
    }

    m_runningStateProcess->setEnvironment(m_environment.toStringList());

    m_runningStateProcess->start(m_deployCmd, args);
}

void BlackBerryApplicationRunner::readRunningStateStandardOutput()
{
    QProcess *process = qobject_cast<QProcess *>(sender());
    process->setReadChannel(QProcess::StandardOutput);
    while (process->canReadLine()) {
        const QString line = QString::fromLocal8Bit(process->readLine());
        if (line.startsWith(QLatin1String("result"))) {
            m_running = parseRunningState(line);
            break;
        }
    }

    if (!m_running)
        reset();
}

void BlackBerryApplicationRunner::reset()
{
    m_pid = -1;
    m_appId.clear();
    m_running = false;
    m_stopping = false;

    m_runningStateTimer->stop();
    if (m_runningStateProcess) {
        m_runningStateProcess->terminate();
        if (!m_runningStateProcess->waitForFinished(1000))
            m_runningStateProcess->kill();
    }

    if (m_logProcessRunner) {
        m_logProcessRunner->stop();

        delete m_logProcessRunner;
        m_logProcessRunner = 0;
    } else {
        emit finished();
    }
}
